Skip to content

fix: branch protection for mergeOnly#61

Merged
obcode merged 1 commit intomainfrom
bugfix/branch_rules
Apr 25, 2026
Merged

fix: branch protection for mergeOnly#61
obcode merged 1 commit intomainfrom
bugfix/branch_rules

Conversation

@obcode
Copy link
Copy Markdown
Owner

@obcode obcode commented Apr 25, 2026

No description provided.

Co-authored-by: Copilot <copilot@github.com>
Copilot AI review requested due to automatic review settings April 25, 2026 16:47
@obcode obcode merged commit b1b139b into main Apr 25, 2026
10 checks passed
@obcode obcode deleted the bugfix/branch_rules branch April 25, 2026 16:47
@github-actions
Copy link
Copy Markdown

Coverage

github.com/obcode/glabs/cmd/archive.go:12:		init				100.0%
github.com/obcode/glabs/cmd/check.go:10:		init				100.0%
github.com/obcode/glabs/cmd/clone.go:45:		init				100.0%
github.com/obcode/glabs/cmd/delete.go:12:		init				100.0%
github.com/obcode/glabs/cmd/generate.go:12:		init				100.0%
github.com/obcode/glabs/cmd/protect.go:12:		init				100.0%
github.com/obcode/glabs/cmd/report.go:14:		init				100.0%
github.com/obcode/glabs/cmd/root.go:39:			Execute				0.0%
github.com/obcode/glabs/cmd/root.go:43:			init				100.0%
github.com/obcode/glabs/cmd/root.go:51:			er				0.0%
github.com/obcode/glabs/cmd/root.go:56:			initConfig			0.0%
github.com/obcode/glabs/cmd/setaccess.go:12:		init				100.0%
github.com/obcode/glabs/cmd/show.go:8:			init				100.0%
github.com/obcode/glabs/cmd/update.go:12:		init				100.0%
github.com/obcode/glabs/cmd/urls.go:22:			init				100.0%
github.com/obcode/glabs/cmd/version.go:10:		init				100.0%
github.com/obcode/glabs/config/assignment.go:11:	String				100.0%
github.com/obcode/glabs/config/assignment.go:24:	GetAssignmentConfig		88.2%
github.com/obcode/glabs/config/assignment.go:80:	RepoSuffix			100.0%
github.com/obcode/glabs/config/assignment.go:94:	RepoBaseName			100.0%
github.com/obcode/glabs/config/assignment.go:102:	RepoNameWithSuffix		100.0%
github.com/obcode/glabs/config/assignment.go:106:	RepoNameForStudent		100.0%
github.com/obcode/glabs/config/assignment.go:110:	RepoNameForGroup		100.0%
github.com/obcode/glabs/config/assignment.go:114:	assignmentPath			100.0%
github.com/obcode/glabs/config/assignment.go:128:	per				100.0%
github.com/obcode/glabs/config/assignment.go:135:	description			100.0%
github.com/obcode/glabs/config/assignment.go:145:	mergeRequest			83.3%
github.com/obcode/glabs/config/course.go:14:		GetCourseConfig			50.0%
github.com/obcode/glabs/config/release.go:8:		release				100.0%
github.com/obcode/glabs/config/release.go:22:		releaseMergeRequest		100.0%
github.com/obcode/glabs/config/release.go:46:		dockerImages			100.0%
github.com/obcode/glabs/config/repo.go:11:		startercode			87.5%
github.com/obcode/glabs/config/repo.go:45:		branches			91.5%
github.com/obcode/glabs/config/repo.go:126:		normalizeBranchRuleConfigKeys	76.5%
github.com/obcode/glabs/config/repo.go:157:		defaultBranch			37.5%
github.com/obcode/glabs/config/repo.go:172:		issues				100.0%
github.com/obcode/glabs/config/repo.go:193:		clone				100.0%
github.com/obcode/glabs/config/repo.go:215:		SetBranch			100.0%
github.com/obcode/glabs/config/repo.go:219:		SetProtectToBranch		88.9%
github.com/obcode/glabs/config/repo.go:237:		SetLocalpath			100.0%
github.com/obcode/glabs/config/repo.go:241:		SetForce			100.0%
github.com/obcode/glabs/config/seeder.go:15:		seeder				53.8%
github.com/obcode/glabs/config/show.go:10:		Show				98.0%
github.com/obcode/glabs/config/students.go:14:		SetAccessLevel			100.0%
github.com/obcode/glabs/config/students.go:28:		accessLevel			100.0%
github.com/obcode/glabs/config/students.go:43:		students			100.0%
github.com/obcode/glabs/config/students.go:75:		mkStudents			94.4%
github.com/obcode/glabs/config/students.go:108:		groups				100.0%
github.com/obcode/glabs/config/urls.go:5:		Urls				100.0%
github.com/obcode/glabs/git/auth.go:11:			GetAuth				90.0%
github.com/obcode/glabs/git/clone.go:18:		Clone				80.0%
github.com/obcode/glabs/git/clone.go:38:		cloneurl			100.0%
github.com/obcode/glabs/git/clone.go:44:		localpath			100.0%
github.com/obcode/glabs/git/clone.go:48:		clone				36.1%
github.com/obcode/glabs/git/starterrepo.go:22:		PrepareStartercodeRepo		0.0%
github.com/obcode/glabs/gitlab/archive.go:14:		Archive				77.8%
github.com/obcode/glabs/gitlab/archive.go:32:		archivePerStudent		90.9%
github.com/obcode/glabs/gitlab/archive.go:54:		archivePerGroup			90.9%
github.com/obcode/glabs/gitlab/archive.go:76:		archive				75.6%
github.com/obcode/glabs/gitlab/branches.go:12:		syncConfiguredBranches		47.4%
github.com/obcode/glabs/gitlab/branches.go:50:		defaultBranchName		37.5%
github.com/obcode/glabs/gitlab/branches.go:68:		isBranchAlreadyExistsError	0.0%
github.com/obcode/glabs/gitlab/check.go:9:		CheckCourse			90.6%
github.com/obcode/glabs/gitlab/check.go:67:		checkStudent			100.0%
github.com/obcode/glabs/gitlab/check.go:89:		checkDupsInGroups		100.0%
github.com/obcode/glabs/gitlab/delete.go:11:		Delete				77.8%
github.com/obcode/glabs/gitlab/delete.go:29:		deletePerStudent		100.0%
github.com/obcode/glabs/gitlab/delete.go:40:		deletePerGroup			100.0%
github.com/obcode/glabs/gitlab/delete.go:51:		delete				92.9%
github.com/obcode/glabs/gitlab/generate.go:14:		Generate			38.9%
github.com/obcode/glabs/gitlab/generate.go:51:		generate			0.0%
github.com/obcode/glabs/gitlab/generate.go:226:		generatePerStudent		0.0%
github.com/obcode/glabs/gitlab/generate.go:239:		generatePerGroup		0.0%
github.com/obcode/glabs/gitlab/gitlab.go:13:		NewClient			80.0%
github.com/obcode/glabs/gitlab/groups.go:12:		getGroupIDByFullPath		100.0%
github.com/obcode/glabs/gitlab/groups.go:33:		getGroupID			100.0%
github.com/obcode/glabs/gitlab/groups.go:46:		createGroup			83.3%
github.com/obcode/glabs/gitlab/issues.go:14:		getStartercodeProject		100.0%
github.com/obcode/glabs/gitlab/issues.go:58:		replicateIssue			100.0%
github.com/obcode/glabs/gitlab/projects.go:12:		generateProject			77.5%
github.com/obcode/glabs/gitlab/projects.go:97:		getProjectByName		80.0%
github.com/obcode/glabs/gitlab/protect.go:15:		ProtectToBranch			77.8%
github.com/obcode/glabs/gitlab/protect.go:33:		protectBranch			75.0%
github.com/obcode/glabs/gitlab/protect.go:104:		hasProtectedBranches		100.0%
github.com/obcode/glabs/gitlab/protect.go:114:		protectSingleBranch		72.2%
github.com/obcode/glabs/gitlab/protect.go:168:		recreateProtectedBranch		80.0%
github.com/obcode/glabs/gitlab/protect.go:206:		replaceBranchPermissions	88.9%
github.com/obcode/glabs/gitlab/protect.go:222:		isProtectedBranchNotFoundError	75.0%
github.com/obcode/glabs/gitlab/protect.go:231:		protectToBranchPerStudent	90.9%
github.com/obcode/glabs/gitlab/protect.go:253:		protectToBranchPerGroup		72.7%
github.com/obcode/glabs/gitlab/report.go:14:		Report				78.9%
github.com/obcode/glabs/gitlab/report.go:51:		ReportHTML			73.7%
github.com/obcode/glabs/gitlab/report.go:86:		ReportJSON			77.8%
github.com/obcode/glabs/gitlab/report_helper.go:17:	report				82.5%
github.com/obcode/glabs/gitlab/report_helper.go:135:	projectReport			85.9%
github.com/obcode/glabs/gitlab/seeder.go:21:		localpath			0.0%
github.com/obcode/glabs/gitlab/seeder.go:25:		runSeeder			0.0%
github.com/obcode/glabs/gitlab/seeder.go:100:		addAndCommit			0.0%
github.com/obcode/glabs/gitlab/seeder.go:121:		push				0.0%
github.com/obcode/glabs/gitlab/setaccess.go:14:		Setaccess			77.8%
github.com/obcode/glabs/gitlab/setaccess.go:32:		setaccess			41.5%
github.com/obcode/glabs/gitlab/setaccess.go:140:	inviteByEmail			100.0%
github.com/obcode/glabs/gitlab/setaccess.go:155:	setaccessPerStudent		100.0%
github.com/obcode/glabs/gitlab/setaccess.go:175:	setaccessPerGroup		80.0%
github.com/obcode/glabs/gitlab/starterrepo.go:14:	pushStartercode			0.0%
github.com/obcode/glabs/gitlab/update.go:15:		Update				60.0%
github.com/obcode/glabs/gitlab/update.go:44:		update				31.6%
github.com/obcode/glabs/gitlab/update.go:91:		updatePerStudent		100.0%
github.com/obcode/glabs/gitlab/update.go:111:		updatePerGroup			100.0%
github.com/obcode/glabs/gitlab/users.go:12:		getUser				95.0%
github.com/obcode/glabs/gitlab/users.go:49:		getUserID			100.0%
github.com/obcode/glabs/gitlab/users.go:63:		addMember			94.1%
github.com/obcode/glabs/main.go:18:			main				0.0%
total:							(statements)			67.8%

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes GitLab branch protection behavior for MergeOnly branch rules by recreating protected-branch rules when switching to “no one can push”, and ensures branch-protection failures are surfaced to callers.

Changes:

  • Recreate protected-branch rules (unprotect + protect) when enforcing merge-only (“no push”) access levels to avoid stale GitLab permissions.
  • Return an error from syncConfiguredBranches when branch protection fails (instead of only logging).
  • Add contract tests covering merge-only rule recreation and error propagation.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
gitlab/protect.go Adds recreateProtectedBranch and uses it to enforce merge-only rules reliably.
gitlab/protect_contract_test.go Adds a contract test asserting merge-only updates recreate (DELETE+POST) rather than PATCH.
gitlab/branches.go Propagates protection failures from syncConfiguredBranches via a wrapped error.
gitlab/branches_contract_test.go Adds a contract test ensuring syncConfiguredBranches returns an error when protection fails.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +12 to +37
func TestSyncConfiguredBranches_ReturnsErrorWhenProtectionFails(t *testing.T) {
client := newContractClient(t, func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPatch && r.URL.Path == "/api/v4/projects/1":
_, _ = w.Write([]byte(`{"id":1,"name":"repo"}`))
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/api/v4/projects/1/protected_branches/main"):
_, _ = w.Write([]byte(`{"id":1,"name":"main","push_access_levels":[{"id":10,"access_level":40}],"merge_access_levels":[{"id":11,"access_level":40}],"unprotect_access_levels":[{"id":12,"access_level":40}]}`))
case r.Method == http.MethodDelete && strings.Contains(r.URL.Path, "/api/v4/projects/1/protected_branches/main"):
w.WriteHeader(http.StatusNoContent)
case r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/api/v4/projects/1/protected_branches"):
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`{"message":"500 Internal Server Error"}`))
default:
w.WriteHeader(http.StatusNotFound)
}
})

cfg := &config.AssignmentConfig{
Branches: []config.BranchRule{{Name: "main", MergeOnly: true, Default: true}},
}
project := &gitlabapi.Project{ID: 1, Name: "repo"}

err := client.syncConfiguredBranches(cfg, project, "main")
if err == nil {
t.Fatal("syncConfiguredBranches() expected error when branch protection fails, got nil")
}
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test only asserts err != nil, so it can pass even if syncConfiguredBranches fails earlier (e.g., default-branch update) and never reaches the branch-protection code path you intend to validate. Tighten the assertion to ensure the error is coming from the protection step (e.g., check that the error message includes the new "error while protecting configured branches" wrapper and/or that the POST /protected_branches endpoint was actually hit).

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +17
case r.Method == http.MethodPatch && r.URL.Path == "/api/v4/projects/1":
_, _ = w.Write([]byte(`{"id":1,"name":"repo"}`))
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/api/v4/projects/1/protected_branches/main"):
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mock server currently only treats PATCH /api/v4/projects/1 as a success. Since this test’s goal is to reach the protection step, consider making this handler accept whatever HTTP method Projects.EditProject actually uses (or at least fail the test if an unexpected method hits this endpoint) so the test can’t silently pass/fail due to a mock mismatch.

Copilot uses AI. Check for mistakes.
Comment thread gitlab/protect.go
Comment on lines +122 to +129
// GitLab can keep stale push permissions when updating existing rules to
// "No one" (merge-only). Recreate the rule to enforce the access levels.
if pushAccessLevel == gitlab.NoPermissions {
if err := c.recreateProtectedBranch(project, branch, pushAccessLevel, mergeAccessLevel); err != nil {
return err
}
return nil
}
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the merge-only path, protectSingleBranch always unprotects and re-protects whenever pushAccessLevel == gitlab.NoPermissions, even if the existing protected-branch rule is already correctly configured. This introduces a small window where the branch is briefly unprotected (race risk) and adds extra API calls. Consider only recreating when the existing rule’s push access levels don’t already represent “no one”, or try an update first and fall back to recreate only if GitLab leaves stale push permissions.

Copilot uses AI. Check for mistakes.
Comment thread gitlab/protect.go
Comment on lines +161 to +165
if err := c.recreateProtectedBranch(project, branch, pushAccessLevel, mergeAccessLevel); err != nil {
return err
}

return nil
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the GET-404 path, protectSingleBranch calls recreateProtectedBranch, which performs a DELETE (unprotect) before POST (protect). When the branch isn’t protected yet, this DELETE is unnecessary and adds an extra API round-trip. Consider calling ProtectRepositoryBranches directly in the not-found case and reserving the delete+recreate logic for the “existing rule but needs reset” scenario.

Copilot uses AI. Check for mistakes.
obcode added a commit that referenced this pull request Apr 26, 2026
…#60)

* feat: implement merge request approval rules and settings

- Added functionality to apply merge request approval rules based on member count.
- Introduced methods to handle approval settings for merge requests.
- Enhanced branch protection logic to consider approval rules.
- Updated tests to cover new approval rule functionalities and edge cases.
- Refactored existing branch protection methods to accommodate new logic.

Co-authored-by: Copilot <copilot@github.com>

* fix: branch protection for mergeOnly (#61)

Co-authored-by: Copilot <copilot@github.com>

* feat: implement merge request approval rules and settings

- Added functionality to apply merge request approval rules based on member count.
- Introduced methods to handle approval settings for merge requests.
- Enhanced branch protection logic to consider approval rules.
- Updated tests to cover new approval rule functionalities and edge cases.
- Refactored existing branch protection methods to accommodate new logic.

Co-authored-by: Copilot <copilot@github.com>

* fix: add missing parameter to syncConfiguredBranches call in test

* refactor: go install v2 (#62)

* feat: implement merge request approval rules and settings

- Added functionality to apply merge request approval rules based on member count.
- Introduced methods to handle approval settings for merge requests.
- Enhanced branch protection logic to consider approval rules.
- Updated tests to cover new approval rule functionalities and edge cases.
- Refactored existing branch protection methods to accommodate new logic.

Co-authored-by: Copilot <copilot@github.com>

* feat: implement merge request approval rules and settings

- Added functionality to apply merge request approval rules based on member count.
- Introduced methods to handle approval settings for merge requests.
- Enhanced branch protection logic to consider approval rules.
- Updated tests to cover new approval rule functionalities and edge cases.
- Refactored existing branch protection methods to accommodate new logic.

Co-authored-by: Copilot <copilot@github.com>

* fix: add missing parameter to syncConfiguredBranches call in test

* refactor: update import paths to use v2 for consistency across the project

Co-authored-by: Copilot <copilot@github.com>

---------

Co-authored-by: Copilot <copilot@github.com>

* fix: update module path to v2 for go install support

* fix: enhance version command to display build information conditionally (#63)

* fix: update troubleshooting documentation and improve approval rule handling for multiple branches

Co-authored-by: Copilot <copilot@github.com>

* docs: add warning about project-wide squash settings in approval rules

Co-authored-by: Copilot <copilot@github.com>

* Update gitlab/branches.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: refine error message check for any-approver rule in merge request approval tests

Co-authored-by: Copilot <copilot@github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants